package cn.bincker.csustcj.edu;

import cn.bincker.csustcj.edu.entity.*;
import cn.bincker.csustcj.edu.exception.LoginExpireException;
import cn.bincker.csustcj.edu.exception.PlatformException;
import cn.bincker.csustcj.edu.utils.EncryptUtils;
import cn.bincker.csustcj.edu.utils.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;
import java.util.regex.Pattern;

import static cn.bincker.csustcj.edu.utils.EncryptUtils.encrypt;

@Slf4j
public class DefaultPlatform implements Platform{
    private final OkHttpClient client;
    private final ObjectMapper objectMapper;
    private final Map<String, String> hiddenValue = new HashMap<>();
    private final DefaultCookieJar cookieJar = new DefaultCookieJar();

    public DefaultPlatform() {
        //noinspection KotlinInternalInJava
        client = new OkHttpClient.Builder()
                .connectTimeout(Duration.ofSeconds(1))
                .readTimeout(Duration.ofSeconds(10))
                .cookieJar(cookieJar)
                .build();
        objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }

    @Override
    public OkHttpClient httpClient() {
        return client;
    }

    @Override
    public ObjectMapper objectMapper() {
        return objectMapper;
    }

    @Override
    public void login(String username, String password) {
        var request = buildRequest(URL_SIGN_IN)
                .header("Referer", URL_LOGIN)
                .post(RequestBody.create(
                        String.format("name=%s&pwd=%s&verifyCode=0", username, password),
                        MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)
                ))
                .build();
        var result = doRequestWithJsonResponse(request, CommonResult.class);
        if (!result.getState()) throw new PlatformException(result.getMsg());
        var html = doRequestWithPlainText(buildRequest(URL_INDEX, "document").header("Referer", URL_INDEX).get().build());
        var jsoup = Jsoup.parse(html);
        var inputs = jsoup.select("input[type=hidden]");
        hiddenValue.clear();
        for (Element input : inputs) {
            hiddenValue.put(input.attr("id"), input.val());
        }
    }

    @Override
    public List<Major> getMajors() {
        return doRequestWithJsonResponse(
                buildRequest(URL_GET_MAJOR)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create(new byte[0]))
                        .build(),
                objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Major.class)
        );
    }

    @Override
    public Message getMessage() {
        return doRequestWithJsonResponse(
                buildRequest(URL_GET_MSG)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create("type=get&msgType=1", MediaType.get(MEDIA_TYPE_FORM_URLENCODED)))
                        .build(),
                Message.class
        );
    }

    @Override
    public ChangeMajorResult changeMajor(Major major) {
        return doRequestWithJsonResponse(
                buildRequest(URL_CHANGE_MAJOR)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create("major_id=" + major.getMajorId(), MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                        .build(),
                ChangeMajorResult.class
        );
    }

    @Override
    public List<Message> getAllMessage() {
        return doRequestWithJsonResponse(
                buildRequest(URL_GET_ALL_MSG)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create(new byte[0]))
                        .build(),
                objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Message.class)
        );
    }

    @Override
    public CommonResult getClassInfo() {
        return doRequestWithJsonResponse(
                buildRequest(URL_GET_CLASS_INFO)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create(new byte[0]))
                        .build(),
                CommonResult.class
        );
    }

    @Override
    public StudentInfo getStudentInfo() {
        var studentId = encrypt(hiddenValue.get("hidStudentId"));
        return doRequestWithJsonResponse(
                buildRequest(URL_GET_STUDENT_INFO)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create("student_id=" + studentId, MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                        .build(),
                StudentInfo.class
        );
    }

    @Override
    public Map<String,List<Course>> getCourses() {
        var result = new HashMap<String, List<Course>>();
        var paramsPattern = Pattern.compile("\\(([^)]+)\\)");
        var html = doRequestWithPlainText(
                buildRequest(URL_ONLINE_LEARNING_NEW_INDEX + "?isCurrent=0&type=0", "document").get()
                        .header("Referer", URL_INDEX)
                        .build()
        );
        var jsoup = Jsoup.parse(html);
        var semesters = jsoup.select(".xq-lists");
        for (Element semester : semesters) {
            var courses = new ArrayList<Course>();
            result.put(semester.select(".single-xq>.single-xq-left").text().trim(), courses);
            var nodes = semester.select(".list-content .list-content-right");
            for (Element node : nodes) {
                var course = new Course();
                course.setName(node.select("h3").text().trim());
                course.setScore(Integer.parseInt(node.select(".score>.score-left>span:nth-child(2)").text()));
                course.setTotalScore(Integer.parseInt(node.select(".score>.score-left>span:nth-child(3)").text()));
                // 课程
                String onclick = node.select("#btnKJ1").attr("onclick");
                String paramStr;
                if (StringUtils.notBlank(onclick)){
                    paramStr = StringUtils.cut(onclick, "(", ")");
                    if(StringUtils.isBlank(paramStr)) throw new PlatformException("invalid course click call: " + onclick);
                    var params = paramStr.split(",");
                    if (params.length != 6) throw new PlatformException("course click function params count error: " + params.length + "\tonclick=" + onclick);
                    course.setCourseCode(removeQuotationMarks(params[0]));
                    course.setLearnTime(removeQuotationMarks(params[1]));
                    course.setLearnTimeEnd(removeQuotationMarks(params[2]));
                    course.setStemCode(removeQuotationMarks(params[3]));
                    course.setIsFaceKJ(!Objects.equals(removeQuotationMarks(params[4]), "0"));
                    course.setTermCourseID(removeQuotationMarks(params[5]));
                }
                // 作业
                onclick = node.select("#btnZY1").attr("onclick");
                if (StringUtils.notBlank(onclick)) {
                    paramStr = StringUtils.cut(onclick, "(", ")");
                    if (StringUtils.isBlank(paramStr))
                        throw new PlatformException("invalid course click call: " + onclick);
                    var params = paramStr.split(",");
                    if (params.length != 7)
                        throw new PlatformException("course click function params count error: " + params.length + "\tonclick=" + onclick);
                    course.setJobCode(removeQuotationMarks(params[0]));
                    course.setJobTime(removeQuotationMarks(params[1]));
                    course.setJobTimeEnd(removeQuotationMarks(params[2]));
                    course.setJobCodeSource(removeQuotationMarks(params[3]));
                    course.setJobMessage(removeQuotationMarks(params[4]));
                }
                // 考试
                onclick = node.select("#btnQM1").attr("onclick");
                if (StringUtils.notBlank(onclick)) {
                    paramStr = StringUtils.cut(onclick, "(", ")");
                    if (StringUtils.isBlank(paramStr))
                        throw new PlatformException("invalid course click call: " + onclick);
                    var params = paramStr.split(",");
                    if(params.length > 0) course.setMid(removeQuotationMarks(params[0]));
                    if(params.length > 1) course.setTime(removeQuotationMarks(params[1]));
                    if(params.length > 2) course.setTimeEnd(removeQuotationMarks(params[2]));
                    if(params.length > 3) course.setFinalExamSource(removeQuotationMarks(params[3]));
                    if(params.length > 4) course.setExamMessage(removeQuotationMarks(params[4]));
                    if(params.length > 5) course.setIsFaceQM(removeQuotationMarks(params[5]));
                    if(params.length > 7) course.setAllowCountCJ(removeQuotationMarks(params[7]));
                    if(params.length > 8) course.setVsCJ(removeQuotationMarks(params[8]));
                    if(params.length > 9) course.setLimitedTimeCJ(removeQuotationMarks(params[9]));
                    if(params.length > 10) course.setResumeCJ(removeQuotationMarks(params[10]));
                    if(params.length > 11) course.setVrCJ(removeQuotationMarks(params[11]));
                }

                courses.add(course);
            }
        }
        return result;
    }

    private static String removeQuotationMarks(String s){
        if (s == null || s.isEmpty()) return s;
        s = s.trim();
        var len = s.length();
        if (s.charAt(0) == '\'' && s.charAt(len - 1) == '\'') return s.substring(1, len - 1);
        if (s.charAt(0) == '"' && s.charAt(len - 1) == '"') return s.substring(1, len - 1);
        return s;
    }

    @Override
    public CommonResult compareLearnTime(Course course, int type) {
        return doRequestWithJsonResponse(
                buildRequest(URL_ONLINE_LEARNING_NEW_COMPARE_TIME)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create(String.format("termcourseID=%s&type=%d", course.getTermCourseID(), type), MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                        .build(),
                CommonResult.class
        );
    }

    @Override
    public CommonResult getCourseUrl(Course course) {
        var result = doRequestWithJsonResponse(
                buildRequest(URL_GET_COURSE_URL)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create(String.format("termcourseID=%s", course.getTermCourseID()), MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                        .build(),
                CommonResult.class
        );
        doRequestWithPlainText(
                buildRequest(result.getMsg(), "document").get()
                        .build()
        );
        return result;
    }

    @Override
    public void exit() {
        doRequestWithJsonResponse(
                buildRequest(URL_EXIT)
                        .header("Referer", URL_INDEX)
                        .post(RequestBody.create(new byte[0]))
                        .build(),
                CommonResult.class
        );
    }

    @Override
    public ScoreDetail getScoreDetail(Course course) {
        var detail = new ScoreDetail();
        var html = doRequestWithPlainText(
                buildRequest(String.format(URL_SCORE_DETAIL, course.getTermCourseID()), "document").get()
                        .header("Referer", URL_INDEX)
                        .build()
        );
        var jsoup = Jsoup.parse(html);
//        读取分数占比
        var elements = jsoup.select("body>div>.table>tbody>tr:nth-child(2)>td");
        if (elements.size() != 8){
            throw new PlatformException("分数详情页改变, 分数占比读取失败: \n" + html);
        }
        detail.getScoreProportions().add(new ScoreProportion(elements.get(0).text(), elements.get(1).text()));
        detail.getScoreProportions().add(new ScoreProportion(elements.get(2).text(), elements.get(3).text()));
        detail.getScoreProportions().add(new ScoreProportion(elements.get(4).text(), elements.get(5).text()));
        detail.getScoreProportions().add(new ScoreProportion(elements.get(6).text(), elements.get(7).text()));
//        读取课件学习时间和成绩
        elements = jsoup.select("body>div>div>.table:nth-child(1)>tbody>tr>td");
        if (elements.size() != 5){
            throw new PlatformException("分数详情页改变，课件学习成绩读取失败: \n" + html);
        }
        detail.setCourseWareLearnTime(elements.get(2).text().trim());
        var numberPattern = Pattern.compile("\\d+(\\.\\d+)?");
        var matcher = numberPattern.matcher(elements.get(3).text());
        if (matcher.find()) detail.setCourseWareLearnScore(Double.parseDouble(matcher.group()));
//        读取作业成绩
        elements = jsoup.select("body>div>div>.table:nth-child(2)>tbody>tr");
        for (int i = 0; i < elements.size() - 1; i++) {
            var tr = elements.get(i + 1);
            var scores = new HomeworkScore();
            scores.setName(tr.child(0).text().trim());
            matcher = numberPattern.matcher(tr.child(1).text());
            if (matcher.find()) scores.setTimes(Integer.parseInt(matcher.group()));
            matcher = numberPattern.matcher(tr.child(2).text());
            if (matcher.find()) scores.setFullMark(Integer.parseInt(matcher.group()));
            matcher = numberPattern.matcher(tr.child(3).text());
            if (matcher.find()) scores.setScore(Double.parseDouble(matcher.group()));
            detail.getHomeworkScores().add(scores);
        }
//        读取平时得分
        elements = jsoup.select("body>div>div>.table:nth-child(3)>tbody>tr>td:nth-child(3)");
        matcher = numberPattern.matcher(elements.text());
        if (matcher.find()) detail.setUsualPerformance(Double.parseDouble(matcher.group()));
//        读取考试得分
        elements = jsoup.select("body>div>div>.table:nth-child(4)>tbody>tr>td:nth-child(3)");
        matcher = numberPattern.matcher(elements.text());
        if (matcher.find()) detail.setExamResult(Double.parseDouble(matcher.group()));
        return detail;
    }

    @Override
    public CourseWare findCourseWareByByCode(Course course, String userId, String clientCode) {
        ResponseResult<CourseWare> result = doRequestWithJsonResponse(
                buildRequest(String.format(URL_FIND_CW_BY_CODE, course.getCourseCode(), userId, clientCode))
                        .header("Referer", URL_CWS_INDEX)
                        .get()
                        .build(),
                objectMapper.getTypeFactory().constructParametricType(ResponseResult.class, CourseWare.class)
        );
        checkError(result, "获取课件失败");
        return result.getData();
    }

    @Override
    public List<CourseWareCatalog> getCourseCatalog(CourseWare courseWare, String userId, String clientCode) {
        var params = new HashMap<String, Object>();
        params.put("coursewareCode", courseWare.getCode());
        params.put("userId", userId);
        params.put("clientCode", clientCode);
        params.put("isQuestion", false);
        var typeFactory = objectMapper.getTypeFactory();
        ResponseResult<List<CourseWareCatalog>> result;
        try {
            result = doRequestWithJsonResponse(
                    buildRequest(URL_CATALOGS)
                            .header("Referer", URL_CWS_INDEX)
                            .post(RequestBody.create(objectMapper.writeValueAsString(params), MediaType.parse(MEDIA_TYPE_APPLICATION_JSON)))
                            .build(),
                    typeFactory.constructParametricType(ResponseResult.class, typeFactory.constructCollectionType(ArrayList.class, CourseWareCatalog.class))
            );
        } catch (JsonProcessingException e) {
            throw new PlatformException("序列化参数失败", e);
        }
        checkError(result, "获取课件目录失败");
        return result.getData();
    }

    private static void checkError(ResponseResult<?> result, String msg) {
        if (!result.getSuccess()){
            if (result.getMessage().contains("登录已失效")){
                throw new LoginExpireException(msg + "：" + result.getMessage() + "\tcode=" + result.getCode());
            }
            throw new PlatformException(msg + "：" + result.getMessage() + "\tcode=" + result.getCode());
        }
    }

    @Override
    public CourseCatalogInfo getCourseCatalogInfo(CourseWareCatalog catalog, String userId, String clientCode) {
        var params = new HashMap<String, Object>();
        params.put("coursewareCode", catalog.getCoursewareCode());
        params.put("userId", userId);
        params.put("clientCode", clientCode);
        params.put("isQuestion", false);
        ResponseResult<CourseCatalogInfo> result;
        try {
            result = doRequestWithJsonResponse(
                    buildRequest(String.format(URL_CATALOG_INFO, catalog.getId()))
                            .header("Referer", URL_CWS_INDEX)
                            .post(RequestBody.create(objectMapper.writeValueAsString(params), MediaType.parse(MEDIA_TYPE_APPLICATION_JSON)))
                            .build(),
                    objectMapper.getTypeFactory().constructParametricType(ResponseResult.class, CourseCatalogInfo.class)
            );
        } catch (JsonProcessingException e) {
            throw new PlatformException("序列化参数失败", e);
        }
        checkError(result, "获取课件目录信息失败");
        return result.getData();
    }

    @Override
    public LearnRecord getUserLearnRecord(String userId, String userName, String clientCode, CourseWare courseWare, CourseCatalogInfo catalogInfo) {
        ResponseResult<LearnRecord> result = doRequestWithJsonResponse(
                buildRequest(String.format(URL_LEARN_RECORD_USER, 0, userId, userName, clientCode, courseWare.getBusinessLineCode(), courseWare.getCode(), courseWare.getName(), catalogInfo.getId(), catalogInfo.getTitle(), catalogInfo.getMediaDuration(), userName))
                        .header("Referer", URL_CWS_INDEX)
                        .get()
                        .build(),
                objectMapper.getTypeFactory().constructParametricType(ResponseResult.class, LearnRecord.class)
        );
        checkError(result, "获取学习记录失败");
        return result.getData();
    }

    @Override
    public LearnRecord postLearnRecord(LearnRecord learnRecord) {
        ResponseResult<LearnRecord> result;
        try {
            result = doRequestWithJsonResponse(
                    buildRequest(URL_LEARN_RECORD)
                            .header("Referer", URL_CWS_INDEX)
                            .post(RequestBody.create(objectMapper.writeValueAsString(learnRecord), MediaType.parse(MEDIA_TYPE_APPLICATION_JSON)))
                            .build(),
                    objectMapper.getTypeFactory().constructParametricType(ResponseResult.class, LearnRecord.class)
            );
        } catch (JsonProcessingException e) {
            throw new PlatformException("序列化参数失败", e);
        }
        checkError(result, "提交学习记录失败");
        return result.getData();
    }

    @Override
    public LearnRecord putLearnRecord(LearnRecord learnRecord) {
        ResponseResult<LearnRecord> result;
        try {
            result = doRequestWithJsonResponse(
                    buildRequest(URL_LEARN_RECORD)
                            .header("Referer", URL_CWS_INDEX)
                            .put(RequestBody.create(objectMapper.writeValueAsString(learnRecord), MediaType.parse(MEDIA_TYPE_APPLICATION_JSON)))
                            .build(),
                    objectMapper.getTypeFactory().constructParametricType(ResponseResult.class, LearnRecord.class)
            );
        } catch (JsonProcessingException e) {
            throw new PlatformException("序列化参数失败", e);
        }
        checkError(result, "提交学习记录失败");
        return result.getData();
    }

    @Override
    public ResponseResult<?> syncLearnRecord(String username, LearnRecord learnRecord) {
        try {
            var result = doRequestWithJsonResponse(
                    buildRequest(Objects.requireNonNull(HttpUrl.parse(String.format(URL_LEARN_RETURN_URL_INDEX, username))), REQUEST_WITH_XML_HTTP_REQUEST, URL_BASE_CWS)
                            .post(RequestBody.create(objectMapper.writeValueAsString(learnRecord), MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                            .build(),
                    ResponseResult.class
            );
            checkError(result, "同步学习记录失败: \n" + HttpUrl.parse(String.format(URL_LEARN_RETURN_URL_INDEX, username)) + "\n" + objectMapper.writeValueAsString(learnRecord));
            return result;
        } catch (JsonProcessingException e) {
            throw new PlatformException("序列化参数失败: " + learnRecord, e);
        }
    }

    @Override
    public List<Examination> getExaminationList(Course course) {
        var html = doRequestWithPlainText(
                buildRequest(String.format(URL_EXAMINATION_INDEX, course.getTermCourseID()), "document").get()
                        .header("Referer", URL_INDEX)
                        .build()
        );
        var jsoup = Jsoup.parse(html);
        var items = jsoup.select(".task_list>ul>li");
        var result = new ArrayList<Examination>();
        for (Element node : items) {
            var examination = new Examination();
            examination.setTitle(node.select(">h3").textNodes().get(0).text());
            var timesStr = node.select(">div:nth-child(2)>span").text().trim();
            examination.setTimes(Integer.valueOf(timesStr));
            var onclick = node.select(".start_testing").attr("onclick");
            var params = StringUtils.cut(onclick, "(", ")").split(",");
            if(params.length > 0) examination.setJobCode(removeQuotationMarks(params[0]));
            if(params.length > 1) examination.setAllowCountCJ(removeQuotationMarks(params[1]));
            if(params.length > 2) examination.setTermCourseID(removeQuotationMarks(params[2]));
            if(params.length > 3) examination.setZYButtonName(removeQuotationMarks(params[3]));
            if(params.length > 4) examination.setTermCourseName(removeQuotationMarks(params[4]));
            if(params.length > 5) examination.setIsStartOrRecord(removeQuotationMarks(params[5]));
            if(params.length > 6) examination.setSign(removeQuotationMarks(params[6]));
            result.add(examination);
        }
        return result;
    }

    @Override
    public CommonResult getExaminationUrl(Examination examination, boolean record) {
        return doRequestWithJsonResponse(
                buildRequest(URL_GET_EXAMINATION_URL)
                        .post(RequestBody.create(
                                String.format(
                                        "ksType=2&termcourseID=%s&courseCode=%s&startOrRecord=%d&signCJ=%s",
                                        examination.getTermCourseID(),
                                        examination.getJobCode(),
                                        record ? 2 : 1,
                                        examination.getSign()
                                ),
                                MediaType.parse(MEDIA_TYPE_FORM_URLENCODED))
                        )
                        .build(),
                CommonResult.class
        );
    }

    @Override
    public List<ExaminationRecord> getExaminationRecord(Examination examination) {
        var url = getExaminationUrl(examination, true);
        String html = doRequestWithPlainText(buildRequest(url.getMsg()).build());
        var jsoup = Jsoup.parse(html);
        var trList = jsoup.select("table>tbody>tr");
        var result = new ArrayList<ExaminationRecord>();
        for (Element tr : trList) {
            var record = new ExaminationRecord();
            var tds = tr.select(">td span");
            record.setTitle(tds.get(0).text().trim());
            record.setStartTime(tds.get(1).text().trim());
            record.setEndTime(tds.get(2).text().trim());
            record.setScore(tds.get(3).text().trim());
            var actions = tr.select("td:nth-child(5)>a");
            for (Element action : actions) {
                String actionTitle = action.text().trim();
                if (actionTitle.equals("查看答卷")){
                    record.setViewAnswerUrl(URL_BASE_EXAM + action.attr("href"));
                }else if(actionTitle.equals("继续考试")){
                    record.setResumeUrl(URL_BASE_EXAM + action.attr("href"));
                }
            }
            result.add(record);
        }
        return result;
    }

    @Override
    public void doExamination(Examination examination, String startExaminationUrl, String viewAnswerUrl) {
        System.out.println("读取考试信息...");
        String html;
        String examinationUrlBase;
        try(var response = doRequestWithResponse(buildRequest(startExaminationUrl).build()); var body = response.body()){
            assert body != null;
            html = body.string();
            HttpUrl redirectUrl = response.request().url();
            examinationUrlBase = redirectUrl.scheme() + "://" + redirectUrl.host();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        var examObjParams = StringUtils.cut(html, "new ExamObj(", ")").split(",");
        if (examObjParams.length < 2) throw new RuntimeException("ExamObj Param error: html=" + html);
        String limitTime = removeQuotationMarks(examObjParams[0]);
        String userExamId = removeQuotationMarks(examObjParams[1]);
        String leftTime = examObjParams.length > 2 ? removeQuotationMarks(examObjParams[2]) : null;
        String clientJudge = examObjParams.length > 3 ? removeQuotationMarks(examObjParams[3]) : null;
        String confuseOrder = examObjParams.length > 4 ? removeQuotationMarks(examObjParams[4]) : null;
        String confuseChoice = examObjParams.length > 5 ? removeQuotationMarks(examObjParams[5]) : null;
        String answerType = examObjParams.length > 6 ? removeQuotationMarks(examObjParams[6]) : null;
        String basePath = examObjParams.length > 7 ? removeQuotationMarks(examObjParams[7]) : null;
        String theirPapersURL = examObjParams.length > 8 ? removeQuotationMarks(examObjParams[8]) : null;
        String extendedDurationURL = examObjParams.length > 9 ? removeQuotationMarks(examObjParams[9]) : null;

        System.out.println("读取考考题...");
        var examUrl = examinationUrlBase + StringUtils.cut(html, "iframe.prop('src','", "'");
        var questions = Jsoup.parse(doRequestWithPlainText(buildRequest(examUrl).build()))
                .select(".ui-question");
        System.out.println("读取答案...");
        if (StringUtils.isBlank(viewAnswerUrl)) {
            System.out.println("交白卷读取答案...");
            var result = doRequestWithJsonResponse(
                    buildRequest(String.format(examinationUrlBase + URL_SUBMIT_EXAMINATION, userExamId))
                            .post(RequestBody.create("", MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                            .build(),
                    ResponseResult.class
            );
            if (!result.getSuccess()) throw new RuntimeException("交卷失败: " + result);
            var retryTimes = 5;
            for (int i = 0; i < retryTimes; i++) {
                var record = getExaminationRecord(examination).get(0);
                if (StringUtils.isBlank(record.getViewAnswerUrl())) {
                    var waitTime = (int) Math.pow(i + 1, 4);
                    System.out.println("等待交卷..." + waitTime + "s");
                    try {
                        Thread.sleep((long) (waitTime * 1000));
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    continue;
                }
                doExamination(examination, record.getResumeUrl(), record.getViewAnswerUrl());
                return;
            }
            throw new RuntimeException("交卷后无法读取答案");
        }
        String viewAnswerUrlBase;
        try(var response = httpClient().newCall(buildRequest(viewAnswerUrl).build()).execute(); var body = response.body()){
            assert body != null;
            html = body.string();
            var redirectUrl = response.request().url();
            viewAnswerUrlBase = redirectUrl.scheme() + "://" + redirectUrl.host();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        doRequestWithPlainText(buildRequest(viewAnswerUrlBase + StringUtils.cut(html, "iframe.prop('src','", "'")).build());
        var answer = doRequestWithJsonResponse(buildRequest(String.format(viewAnswerUrlBase + URL_GET_EXAMINATION_ANSWER, userExamId)).build(), ExaminationAnswer.class);
        if (!answer.getSuccess()) throw new RuntimeException("读取答案失败");
        List<ExaminationAnswer.Answer> answerList = answer.getAnswers();
        var random = new Random();
        for (int i = 0; i < answerList.size(); i++) {
            ExaminationAnswer.Answer answerItem = answerList.get(i);
            if (StringUtils.isBlank(answerItem.getAnswer())) continue;
            var waitTime = 3 + random.nextInt(0,5);
            System.out.println("延迟作答" + waitTime + "s...");
            try {
                //noinspection BusyWait
                Thread.sleep(waitTime * 1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            var sTime = String.valueOf(System.currentTimeMillis());
            var psqId = questions.select("#q_" + answerItem.getQuestionId()).attr("code").substring(4);
            var answerValue = answerItem.getAnswer();
            var data = String.format(
                    "psqId=%s&answer=%s&attach=&stime=%s&m=%s",
                    psqId,
                    URLEncoder.encode(answerValue, StandardCharsets.UTF_8),
                    sTime,
                    EncryptUtils.md5(String.format(
                            "answer=%s&psqId=%s&stime=%s&key=%s%s",
                            answerValue,
                            psqId,
                            sTime,
                            answerItem.getQuestionId(),
                            userExamId
                    ))
            );
            System.out.println("答题" + (i + 1) + "/" + questions.size() + "...");
            var result = doRequestWithJsonResponse(
                    buildRequest(String.format(examinationUrlBase + URL_SUBMIT_EXAMINATION_ANSWER, userExamId, answerItem.getQuestionId()))
                            .post(RequestBody.create(data, MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                            .build(),
                    ResponseResult.class
            );
            if (!result.getSuccess()) throw new RuntimeException("答题失败: answer=" + answerValue + "\t" + result);
        }
        System.out.println("交卷...");
        var result = doRequestWithJsonResponse(
                buildRequest(String.format(examinationUrlBase + URL_SUBMIT_EXAMINATION, userExamId))
                        .post(RequestBody.create("", MediaType.parse(MEDIA_TYPE_FORM_URLENCODED)))
                        .build(),
                ResponseResult.class
        );
        if (!result.getSuccess()) throw new RuntimeException("交卷失败: " + result);
    }
}
